package com.shujia.flink.state

import org.apache.flink.api.common.eventtime.WatermarkStrategy
import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.connector.kafka.source.KafkaSource
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer
import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend
import org.apache.flink.runtime.state.hashmap.HashMapStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

object Demo2ValueState {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment


    /**
     * 开启checkpoint
     *
     */

    // 每 5000ms 开始一次 checkpoint
    env.enableCheckpointing(20000)

    // 高级选项：

    // 设置模式为精确一次 (这是默认值)
    env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)

    // 确认 checkpoints 之间的时间会进行 500 ms
    env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500)

    // Checkpoint 必须在一分钟内完成，否则就会被抛弃
    env.getCheckpointConfig.setCheckpointTimeout(60000)

    // 允许两个连续的 checkpoint 错误
    env.getCheckpointConfig.setTolerableCheckpointFailureNumber(2)

    // 同一时间只允许一个 checkpoint 进行
    env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)

    // 使用 externalized checkpoints，这样 checkpoint 在作业取消后仍就会被保留
    env.getCheckpointConfig.setExternalizedCheckpointCleanup(
      ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)


    //设置状态后端，保存状态的位置
    //HashMapStateBackend:将状态保存在内存中
    //env.setStateBackend(new HashMapStateBackend())

    //使用rocksDB作为状态后端，可以使用增量快照
    env.setStateBackend(new EmbeddedRocksDBStateBackend(true))


    //状态持久化位置
    env.getCheckpointConfig.setCheckpointStorage("hdfs://master:9000/flink/checkpoint")


    val source: KafkaSource[String] = KafkaSource
      .builder[String]
      //kafka 集群列表
      .setBootstrapServers("master:9092,node1:9092,node2:9092")
      //消费的topic
      .setTopics("lines")
      //消费者组
      .setGroupId("my-group")
      //读取数据的位置，earliest：从最早读取数据，latest：读取最新数据
      .setStartingOffsets(OffsetsInitializer.earliest)
      .setValueOnlyDeserializer(new SimpleStringSchema())
      .build

    //使用kafka 数据源
    val linesDS: DataStream[String] = env
      .fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka Source")


    val wordsDS: DataStream[String] = linesDS.flatMap(_.split(","))

    val keyByDS: KeyedStream[String, String] = wordsDS.keyBy(word => word)


    val wordCountDS: DataStream[(String, Int)] = keyByDS
      .process(new KeyedProcessFunction[String, String, (String, Int)] {


        /**
         * ValueState:单值状态，为每一个key再状态中保存一个值
         * 状态和普通变量的区别：
         * 1、普通变量的数据在内存中不会持久化，任务结束数据就丢失
         * 2、状态的数据默认也在内存中，但是会定时被checkpoint持久化到hdfs中，任务结束，状态不会丢失(需要开启checkpoint之后才能生效)
         *
         */
        var count: ValueState[Int] = _

        /**
         * open:再task初始化的是偶执行，可以用于创建数据库连接和初始化状态
         *
         * @param parameters ：配置文件对象
         */

        override def open(parameters: Configuration): Unit = {

          //获取flink环境对象
          val context: RuntimeContext = getRuntimeContext

          /**
           * 初始化状态
           *
           */

          //创建状态描述对象，需要指定转台的类型，和状态名称
          val stateDesc = new ValueStateDescriptor[Int]("count", classOf[Int])

          //创建状态对象
          count = context.getState(stateDesc)

        }

        override def processElement(word: String,
                                    ctx: KeyedProcessFunction[String, String, (String, Int)]#Context,
                                    out: Collector[(String, Int)]): Unit = {


          //获取状态中保存的数据
          var wordcount: Int = count.value()

          //累加计算
          wordcount += 1

          //更新状态中的数据
          count.update(wordcount)

          //将数据发生到下游
          out.collect(word, wordcount)
        }
      })

    wordCountDS.print()

    env.execute()

  }

}
